package vivace.helper;

import javax.sound.midi.Track;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.MidiMessage;
import vivace.model.*;
import java.awt.Graphics;

import java.util.Vector;
import java.util.ListIterator;
import java.util.HashMap;
import java.util.Set;

import vivace.model.TimeSignature;

/**
 * Helper methods for the notation view
 *
 */
public class NoteHelper {

    public static final boolean PRINT_PAUSE_DEBUG = false;
    public static final boolean PRINT_NOTE_DEBUG = false;
    public static final boolean PRINT_IS_NORMAL_DEBUG = false;
    public static final boolean PRINT_BEAM_DEBUG = false;
    public static final boolean PRINT_OTHER_DEBUG = false;
    public static final boolean PRINT_METHOD_ENTERING_DEBUG = false;
    public static final boolean PRINT_MIDI_MESSAGE_DEBUG = false;
    public static final boolean PRINT_GENERAL_DEBUG = false;

    public static void print(String s){
        if(PRINT_GENERAL_DEBUG) 
                System.out.printf("%s",s);
    }
    public static void print(String s, byte Byte){
        if(PRINT_MIDI_MESSAGE_DEBUG) 
                System.out.printf(">%s%h<",s,Byte);
    }

    public static void print(String s, MidiMessage mm){
        if(PRINT_MIDI_MESSAGE_DEBUG){
                int messageLength = mm.getLength();
                byte message[] = mm.getMessage();
                int status = mm.getStatus();
                System.out.printf("«%s%s%x",
                                  s,
                                  status == ShortMessage.NOTE_ON ? "(ON)" :
                                  status == ShortMessage.NOTE_OFF ? "(OFF)" :
                                  "",
                                  message[0]);
                for(int i=1;messageLength>i;i++)
                    System.out.printf(",%x",message[i]);
                System.out.print("»\n");
        }
    }
    public static void print(String s,Note note){
        if(PRINT_NOTE_DEBUG) 
			System.out.printf("{%s%s[%d:%h]%s}",
			                  s,
			                  note.getNoteTiedFrom()==null?"":note.getNoteTiedFrom().index+"<",
			                  note.index,
			                  (long) (note.getLength()/(note.getRes()*0.125)),
			                  note.getNoteTiedTo()==null?"":">"+note.getNoteTiedTo().index
			                 );
		
    }

    public static void print(String s,Pause pause){
        if(PRINT_PAUSE_DEBUG) 
			System.out.printf("[%s[%h]]",
			                  s,
			                  (long) (pause.getLength()/(pause.getRes()*0.125))
			                 );
		
    }

    public static void print(String s,MusicalObject m){
        if(m instanceof Pause) 
            print(s,(Pause)m);
        else if(m instanceof Note) 
                print(s,(Note)m);
        else if(PRINT_OTHER_DEBUG)
            System.out.printf("[%s]", s);

    }

    /**
     * @param m a Vector of MusicalObjects, to be traversed backwards. (This is due to efficiency.)
     * @return A Vector of Bars to be drawn.
     */
    public static Vector<Bar> createBars(Vector<MusicalObject> m, long PPQResolution){


        //BarBeat beat = new BarBeat(new TimeSignature(PPQResolution),PPQResolution);
        TimeSignature ts = new TimeSignature(PPQResolution);
        if(PRINT_METHOD_ENTERING_DEBUG) {
            System.out.printf("<createBars>\n");
        }
        Vector<Bar> bars = new Vector<Bar>();
        Bar bar = new Bar(ts);


        //for(MusicalObject o:m){
        ListIterator<MusicalObject> i = m.listIterator(m.size());
        if(!i.hasPrevious()) {
            //System.out.println("No can has music‽");
        } else {
            for(MusicalObject o = i.previous(); ; o=i.previous()){
                //////////
                // Adding of notes
                //////////
                if(o instanceof Note){
                    Note note = (Note)o;
                    if(bar.spaceLeft() == 0){
                        bars.add(bar);
                        bar = new Bar(ts);
                    }
                    if (note.getLength() <= bar.spaceLeft()){
                        // if note fits within the current bar, simply add it
                        bar.add(note);

                    } else {
                        // if note is too long, split it into two notes and tie them
                        Note note2 = note.splitAfter(bar.spaceLeft());
                        note2.index=note.index+1;
                        bar.add(note);

                        bars.add(bar);
                        bar = new Bar(ts);

                        while (note2.getLength() > bar.spaceLeft()){
                            //Note note3 = note2.chopOff(bar.spaceLeft());
                            Note note3 = note2.splitAfter(bar.spaceLeft());
                            //note3.index=++index;
                            note3.index = note2.index+1;

                            bar.add(note2);
                            bars.add(bar);
                            bar = new Bar(ts);

                            note2 = note3;
                        }
                        //This note is possibly shorter than a bar. Therefore, do not start a new bar now, but leave it up to the next iteration.
                        print("N",note2);
                        bar.add(note2);
                    }
                } else if(o instanceof Pause){
                    //////////
                    // Adding of pauses
                    //////////
                    Pause pause = (Pause)o;
                    if (pause.getLength() < bar.spaceLeft()){
                        bar.add(pause);



                    } else if(pause.getLength() == bar.spaceLeft()){
                        bar.add(pause);
                        bars.add(bar);
                        bar = new Bar(ts);
                    }


                    else {
                        // if pause is too long, split it into two pauses and tie them
                        Pause pause2 = pause.splitAfter(bar.spaceLeft());

                        bar.add(pause);
                        bars.add(bar);
                        bar = new Bar(ts);

                        // if pause spans several bars, keep splitting and tieing
                        while (pause2.getLength() >= bar.spaceLeft()){
                            bar.add(pause2.splitBefore(bar.spaceLeft()));
                            bars.add(bar);
                            bar = new Bar(ts);
                        }
                        if(pause2.getLength() > 0){
                            bar.add(pause);
                        }
                    }
                }
                else if (o instanceof TimeSignature){
                    //////////
                    // Adding of beats
                    //////////
                    ts = (TimeSignature) o;
                    // a change of beat implies a change of bar...
                    if(!bar.isEmpty()){
                        bars.add(bar);
                        bar = new Bar(ts);
                    }
                    bar.add(ts);
                }

                if(!i.hasPrevious()) {
                    break;
                }
            }
        } 


        // If we come here with an non-empty bar, the midi track has "trailing notes" that needs to be added.
        // In other words: if the notes do not fill up complete bars, we need to add an incomplete bar at the end.
        if(!bar.isEmpty()){
            // fill the bar with pauses.
            bar.add(new Pause(bar.spaceLeft(),PPQResolution));
            // add it.
            bars.add(bar);
        }
        if(PRINT_METHOD_ENTERING_DEBUG) {
            System.out.printf("\n</createBars>\n");
        }
        return bars;
    }





    private static boolean isNormal(MusicalObject n){
        int i;
        // Om n innehåller en och endast en tvåpotens av 32-delar, så är noten "normal", dvs inte punkterad.
        // Noter med längd 0 är axiomatiskt normala.
        for(i = (int) (n.getLength()/(n.getRes()*0.125f));!(i%2 == 1|| i==0); i>>=1) {
            ;
        }
        if(PRINT_IS_NORMAL_DEBUG) {
            System.out.printf("/%h/",i);
        }
        if(i>1) {
            return false;
        }
        else {
            return true;
        }
    }


    // FIXME: This, or some other part of the code, breaks when we try to add some values to an empty bar. Among note lengths resulting in a lack of a 32 pause:
    // 3/16
    // 9/16
    // 11/16
    // 13/16
    // 15/16
    //
    // Ok length, but with the 1/16 pause displayed as a 1/32 pause:
    // 1/16
    // 5/16
    // 7/16
    //
    public static Vector<Pause> normalize(Pause p){
        Vector<Pause> po = new Vector<Pause>();

        long length = p.getLength();
        long resolution = p.getRes();

        if(isNormal(p)) {
            po.add(p);
        } else{
            float treshold = resolution * 0.125f;
            for(int hsdnum = (int) (length/treshold), pauseLength=1; hsdnum >0; hsdnum >>= 1, pauseLength<<=1){
                if(hsdnum%2 == 1){
                    if(p.getLength() == (int)(pauseLength*treshold)){
                        po.add(p);
                        break;
                    }
                    Pause pause = p.splitBefore((int)(pauseLength*treshold)); 
                    po.add(pause);
                }
            }               
            if( length/treshold - (int)(length/treshold) != 0 ){
                int extra = (int)((length/treshold - (int)(length/treshold)) * treshold);
                Pause smallest = po.get(po.size()-1);
                smallest.setLength(smallest.getLength()+extra);
            }
        }
        return po;

    }

    public static Vector<Note> normalize(Note n){


        Note tempnote = null;

        int kk=n.index;
        int k=0;
        long length = n.getLength();
        long resolution = n.getRes();

        Vector<Note> no = new Vector<Note>();

        if (isNormal(n)) {
            no.add(n);
        } else {
            /////
            //Algorithm to split up a note into a multiple of smaller notes if the note is composed of several notes, e.g. a dotted note.
            ////

            float noteResolution = resolution * 0.125f;
            /// Split the note in hemisemidemiquavers.
            /// Take the binary value of the number of hemidemisemiquavers (hdsnum), and add a tied note for each two potence
            /// in this number, and with the corresponding two potence value in length.
            for(int hsdnum = (int) (length/noteResolution), noteLength=1; hsdnum >0; hsdnum >>= 1, noteLength<<=1){
                //                System.out.printf("[%h|%d]",hsdnum,i);
                // if hdsnum is odd, we have shifted a 1 into the least significant position, and it is time to split off a
                // new note, with a length corresponding to 2^(the number of shifts we've done so far) hemidemisemiquavers.
                if(hsdnum%2 == 1){
                    //                    System.out.printf("2");
                    // if the length of what is left of the original note is exactly as long as the note we would've created
                    // here, add the original note instead of creating a new one, and exit.
                    if(n.getLength() == (int)(noteLength*noteResolution)){
                        no.add(n);
                        break;
                    }
                    // otherwise, split off a note with the current length from the original note, tiing it to the last note
                    // we added, if any, and add this split off note to our list of notes.
                    Note note = n.splitBefore((int)(noteLength*noteResolution)); 
                    // This is strictly for debug output. Can be removed if neccessary.
                    note.index = kk+(k+=100);

                    if(tempnote !=null) {
                        tempnote.tieTo(note);
                    }
                    tempnote = note;
                    no.add(tempnote);
                }
            }   
            // If we have smaller notes than our theshold allows, we add their length to the smallest note, to not screw up bar filling.
            if( length/noteResolution - (int)(length/noteResolution) != 0 ){
                int extra = (int)((length/noteResolution - (int)(length/noteResolution)) * noteResolution);
                Note smallest = no.get(no.size()-1);
                smallest.setLength(smallest.getLength()+extra);
            }
        }
        return reverse(no);

    }

    public static Vector<Note> reverse(Vector<Note> no){
        // This reverse operation will be somewhat special, since it reverses the order of a group of notes, but maintains any links outwards, so that the link pointing 
        // from the end note will be the link pointing from the new end note when the reverse operation is finished.
        // This is needed because this function most likely will be called for some but not all notes in a chain of tied notes, as to not break link coherency and consistancy.
        // Let's make this reverse operation reflexive!
        // If we only have one note, we simply return it without changing anything!
        if(no.size() == 1) {
            return no;
        }

        Vector<Note> mo = new Vector<Note>();

        for(Note n:no){
            n.reverse();
            mo.add(0,n);
        }
        Note tmp = mo.get(0).getNoteTiedFrom();
        mo.get(0).unsafeSetNoteTiedFrom(mo.get(mo.size()-1).getNoteTiedTo());
        mo.get(mo.size()-1).unsafeSetNoteTiedTo(tmp);
        return mo;
    }


    /**
     * @param t0 The track where MetaMessages reside.
     * @param t1 The track we want to extract all MusicalObjects from.
     * @param resolution The resolution in PPQ.
     * @return a Vector of MusicalObjects, in reverse order from how they appear in the track.
     */
    // Todo: thange t1,t2 to a Track[], implementing selection of an arbitary number of tracks.
    public static Vector<MusicalObject> createMusicalObjectList(Track t0,Track t1, long resolution){
        if(PRINT_METHOD_ENTERING_DEBUG) {
            System.out.printf("<createMusicalObjects>\n");
        }
        Vector<MidiEvent> messageEvents;
        // key: note value 
        // value: start tick
        HashMap<Integer, Long> tmp = new HashMap<Integer, Long>(128);
        // Every time we initiate the note system, we need to start with an empty bar list!

        //if(t0 != t1)
        //    messageEvents = ProjectHelper.allMidiEvents(t0,t1);
        //else
            messageEvents = ProjectHelper.allMidiEvents(t0, t1);


        MidiMessage mm;
        // Create a listIterator to be traversed backwards.
        ListIterator<MidiEvent> iter = messageEvents.listIterator(messageEvents.size());
        System.out.print(messageEvents.size());

        int noteNumber;
        long length;

        Vector<MusicalObject> mo = new Vector<MusicalObject>();

        byte NOTE_ON = (byte) 0x90;
        byte NOTE_OFF = (byte) 0x80;
        byte RESET_OR_META = (byte) 0xFF;
        byte BEAT_CHANGE = (byte) 0x58;
        byte F0 = (byte) 0xF0; // for(;;){System.out.print("Java sucks!");

        // Traverse the event list in REVERSE order.
        // TODO: Include support for composite notes, accords and such...
        long lastNotePos= -1;
        int kk = 0;
        Note tieNote = null;
        while (iter.hasPrevious()) { 

            while (iter.hasPrevious()) { 
                MidiEvent e = iter.previous();

                mm = (MidiMessage) e.getMessage();
                int messageLength = mm.getLength();
                byte message[] = mm.getMessage();

                // message[0] &= F0; 


                // if we get an end of a note
                if ( 
                    ((message[0] & F0) == NOTE_OFF) || 
                    (((message[0] & F0) == NOTE_ON) && (message[2] == 0))
                   ){

                    print("O",mm);

                    noteNumber = message[1];

                    if(tmp.isEmpty() && lastNotePos !=-1){
                        //Pauses here
                        //For now, filter out pauses that are below our treshold.
                        //TODO: Make this being based on system resolution, somehow.
                        if(lastNotePos-e.getTick() > resolution*0.125){
                            Pause pause = new Pause(lastNotePos-e.getTick(), resolution);
                            print("C",pause);
                            mo.add(pause);
                        }
                    } 
                    // if we get two ends in a row, ignore the second one.
                    if(!tmp.containsKey(noteNumber)) {
                        tmp.put(noteNumber, e.getTick());
                    }

                    // if we get a start of a note..
                } else if( (message[0] & F0) == NOTE_ON && message[2] > 0 ){
                    print("N",mm);
                    noteNumber = message[1];
                    // if we have received an end of a note already, calculate note length
                    if (tmp.containsKey(noteNumber)) {
                        // This is the last note we received. Used to calculate pauses.
                        lastNotePos = e.getTick();

                        // calculate note length
                        length =  -e.getTick() + tmp.get(noteNumber);

                        // No need to do this twize, I think...
                        //tmp.remove(noteNumber);



                        //Note tempnote = null;

                        if(PRINT_NOTE_DEBUG)
                            kk+=1000;
                        Note tempnote = new Note(noteNumber, length, resolution);
                        if(PRINT_NOTE_DEBUG)
                            tempnote.index = kk;

                        if(tieNote != null)
                            tempnote.tieTo(tieNote);
                        mo.add(tempnote);
                        //tempnote = null;

                        // remove corresponding data from the tmp collection
                        tmp.remove(noteNumber);

                    } else 
                        // It seems we have received an endless note. Let's ignore it for now...
                    {
                    }
                } else if(message[0] == RESET_OR_META){
                    if(message[1] == BEAT_CHANGE){
                        
                        
                        if(!tmp.isEmpty()){
                            Set<Integer> notes = tmp.keySet();
                            //Iterator<Integer> it = notes.iterator();
                            for(Integer i:notes){
                                length = tmp.get(i)-e.getTick();
                                Note tempnote = new Note(i, length, resolution);
                                mo.add(tempnote);
                                tieNote = tempnote;
                                tmp.put(i,e.getTick());
                            }
                        }
                        // FIXME: Om det finns oavslutade noter i tmp, lägg till dem här, och se till så att de binds ihop med "nästa" delen av noten!
                        // tmp.getbla
                        // tmp.put(current ticks()-1)
                        // new note(bla-current ticks)
                        // etc
                        

                        TimeSignature ts = new TimeSignature(message, resolution);
                        mo.add(ts);
                        print("B",mm);
                    } else
                        print("X",mm);
                } else {
                    print("_",mm);
                }

            }
        }

        //This creates a pause from the start of the piece to where the first note occurs.
        if( lastNotePos != 0){
            Pause initialPause = new Pause(lastNotePos, resolution);
            mo.add(initialPause);
        }
        if(PRINT_METHOD_ENTERING_DEBUG) {
            System.out.printf("\n</createMusicalObjects>\n");
        }
        return mo;
    }
    /**
     * Paints a tie between the note at position startX and the one at endX, taking
     * into account the current font size, and using the note position y.
     * In case you don't want to set the curvature of the tie yourself, the default value
     * is 8.
     */

    public static void drawArc(int startX, int endX, int y, Graphics g){
        drawArc(startX,endX,y,8,g);
    }

    /**
     * Paints a tie between the note at position startX and the one at endX, taking
     * into account the current font size, and using the note position y.
     * In case you don't want to set the curvature of the tie yourself, the default value
     * is 8.
     */

    public static void drawArc(int startX, int endX, int y, int curvature, Graphics g){
        int arclength = endX - startX;

        int arcStartingAngle = 200;
        int arcExtent = 150;

        // sätt noteOffset till längden på en halv helnot, dvs börja rita bågen en halv not in.
        int noteOffset = g.getFontMetrics().stringWidth("\uD834\uDD5D")/2;

        // ritar ett antal bågar som tillsammans blir en snygg båge som är tjockare i mitten än i kanterna.
        // +=2 är för att det inte skall bli avrundningsfel i hörnen, utan att alla skall börja och sluta i samma punkt.
        for(int i = 0;i<5;i+=2)
        {
            if(PRINT_BEAM_DEBUG) {
                System.out.printf("drawArc(%d,%d,%d,%d,%d,%d)", startX+noteOffset,y-(curvature+i)/2,arclength,curvature+i,arcStartingAngle,arcExtent);
            }
            g.drawArc(startX+noteOffset,y-(curvature+i)/2,arclength,curvature+i,arcStartingAngle,arcExtent);
        }

    }
    }
